package lapsnake.model.data;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;

import owg.util.Calc;
import owg.util.data.KryoInstance;

/**A basic property. Objects may listen to changes to the property, or change its value, notifying other listeners.*/
public class Property<T> {
	private static final HashSet<Class<?>> immutableClasses = new HashSet<>();
	static
	{
		immutableClasses.add(Byte.class);
		immutableClasses.add(Short.class);
		immutableClasses.add(Integer.class);
		immutableClasses.add(Long.class);
		immutableClasses.add(Boolean.class);
		immutableClasses.add(Float.class);
		immutableClasses.add(Double.class);
		immutableClasses.add(String.class);
		//Enum: special case
	}
	/**
	 * Call this method to signify that property values of the given type will not be modified by the application.<br>
	 * This means that instances of the given types will not be cloned when using the get/set methods of properties with these types.<br>
	 * <br>
	 * By default, Java's primitive types, boxed types, strings and enums are considered immutable.
	 * @param safeType A class whose instances never need to be copied.
	 */
	public static void registerImmutableClass(Class<?> safeType)
	{
		immutableClasses.add(safeType);
	}
	/**The type of the property*/
	private Class<T> type;
	/**Whether there are any listeners that have not been notified of a new change.*/
	private boolean dirty = false;
	/**The current value*/
	private T value;
	/**Typed listeners*/
	private ArrayList<WeakReference<TypedListener<? super T>>> typedListeners = new ArrayList<>();
	
	/**Create a new property with the given initial value. <b>It is illegal to pass a null value to this constructor. 
	 * Also, the value type must be an exact match for the generic type.</b>*/
	@SuppressWarnings("unchecked")
	public Property(T initialValue)
	{
		this(null, (Class<T>)initialValue.getClass(), initialValue);
	}
	/**Create a new property with the given initial value. <b>It is illegal to pass a null value to this constructor. 
	 * Also, the value type must be an exact match for the generic type.</b> <br><br>
	 * Optionally, a listener may be added immediately.*/
	@SuppressWarnings("unchecked")
	public Property(TypedListener<? super T> listener, T initialValue)
	{
		this(listener, (Class<T>)initialValue.getClass(), initialValue);
	}
	/**Create a new property with the given type and no initial value.*/
	public Property(Class<T> type) {
		this(null, type, null);
	}
	/**Create a new property with the given type and initial value.*/
	public Property(Class<T> type, T initialValue) {
		this(null, type, initialValue);
	}
	/**Create a new property with the given type and initial value.<br><br> 
	 * Optionally, a listener may be added immediately.*/
	public Property(TypedListener<? super T> listener, Class<T> type, T initialValue) {
		assert (!type.isPrimitive());
		this.type = type;
		value = initialValue;
		if(listener != null)
			addListener(listener);
	}
	
	/**
	 * Set the current value. If the type is not registered as an immutable type ({@link #registerImmutableClass(Class)}) 
	 * then a copy (via Kryo) will be used internally.
	 * @param value The new value for the property. May be null if this makes sense in the application.
	 * @param exclude An Listener which will <b>not</b> receive an action event when this is called. May be null. Is not relevant if fireEvents is false.
	 * This is useful to avoid infinite recursion if the value is changed from an action event handler.
	 * @param fireEvents Whether to fire any action events at all.
	 */
	public void set(T value, Object exclude, boolean fireEvents)
	{
		T oldValue = this.value;
		
		if(value == null || immutableClasses.contains(value.getClass()) || value.getClass().isEnum())
			this.value = value;
		else
			this.value = KryoInstance.kryo.copy(value);
		if((oldValue == null && value != null) || !oldValue.equals(value))
		{
			dirty = true;
			
			Iterator<WeakReference<TypedListener<? super T>>> it = typedListeners.iterator();
			while(it.hasNext())
			{
				TypedListener<? super T> next = it.next().get();
				System.out.println(this+"CHaNGE notify: "+next);
				if(next != null)
				{
					if(next != exclude)
					{
						next.valueChanged(this, oldValue);
					}
					if(!dirty)
						return;
				}
				else
					it.remove();//Clean up destroyed reference
			}
			dirty = false;
		}
	}
	/**Get the current value. If the type is not registered as an immutable type ({@link #registerImmutableClass(Class)}) then a copy (via Kryo) will be returned.*/
	public T get()
	{
		if(value == null || immutableClasses.contains(value.getClass()) || value.getClass().isEnum())
			return value;
		else
			return KryoInstance.kryo.copy(value);
	}
	
	/**Listen to changes to this property. It is illegal to add a listener twice. It is illegal to add null as a listener.
	 * This will not prevent the listener from being garbage collected. It will be automatically removed if this happens.*/
	public void addListener(TypedListener<? super T> l)
	{
		if(l == null)
		{
			System.err.println("Cannot add null as action listener to "+this);
			return;
		}
		int oldIndex = Calc.indexOfWeakReference(typedListeners, l);
		if(oldIndex != -1)
		{
			System.err.println("Warning: Tried to add "+l+" as listener to "+this+" but it was already added at "+oldIndex);
			return;
		}
		typedListeners.add(new WeakReference<TypedListener<? super T>>(l));
	}
	
	/**Remove the indicated listener. Returns false if it has not been added yet. 
	 * Note that listeners will be removed automatically if they are garbage collected.*/
	public boolean removeListener(TypedListener<T> l)
	{
		int index = Calc.indexOfWeakReference(typedListeners, l);
		if(index == -1)
			return false;
		typedListeners.remove(index);
		return true;
	}
	/**Returns the type of the property value*/
	public Class<T> getType()
	{
		return type;
	}
	@Override
	public String toString() {
		assert type != null;
		return "P: "+type.getSimpleName()+"="+(value==null?"null":value.toString());
	}
}
